package com.lody.virtual.server.accounts;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorDescription;
import android.accounts.IAccountAuthenticator;
import android.accounts.IAccountAuthenticatorResponse;
import android.accounts.IAccountManagerResponse;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
import android.util.Xml;

import com.lody.virtual.client.core.VirtualCore;
import com.lody.virtual.helper.compat.AccountManagerCompat;
import com.lody.virtual.helper.utils.VLog;
import com.lody.virtual.os.VBinder;
import com.lody.virtual.os.VEnvironment;
import com.lody.virtual.os.VUserHandle;
import com.lody.virtual.server.IAccountManager;
import com.lody.virtual.server.am.VActivityManagerService;
import com.lody.virtual.server.pm.VPackageManagerService;

import org.xmlpull.v1.XmlPullParser;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import mirror.com.android.internal.R_Hide;

import static android.accounts.AccountManager.ERROR_CODE_BAD_ARGUMENTS;


/**
 * @author Lody
 */
public class VAccountManagerService extends IAccountManager.Stub {

	private static final AtomicReference<VAccountManagerService> sInstance = new AtomicReference<>();
	private static final long CHECK_IN_TIME = 30 * 24 * 60 * 1000L;
	private static final String TAG = VAccountManagerService.class.getSimpleName();
	private final SparseArray<List<VAccount>> accountsByUserId = new SparseArray<>();
	private final LinkedList<AuthTokenRecord> authTokenRecords = new LinkedList<>();
	private final LinkedHashMap<String, Session> mSessions = new LinkedHashMap<>();
	private final AuthenticatorCache cache = new AuthenticatorCache();
	private Context mContext = VirtualCore.get().getContext();
	private long lastAccountChangeTime = 0;


	public static VAccountManagerService get() {
		return sInstance.get();
	}

	public static void systemReady() {
		VAccountManagerService service = new VAccountManagerService();
		service.readAllAccounts();
		sInstance.set(service);
	}


	private static AuthenticatorDescription parseAuthenticatorDescription(Resources resources, String packageName,
																		  AttributeSet attributeSet) {
		TypedArray array = resources.obtainAttributes(attributeSet, R_Hide.styleable.AccountAuthenticator.get());
		try {
			String accountType = array.getString(R_Hide.styleable.AccountAuthenticator_accountType.get());
			int label = array.getResourceId(R_Hide.styleable.AccountAuthenticator_label.get(), 0);
			int icon = array.getResourceId(R_Hide.styleable.AccountAuthenticator_icon.get(), 0);
			int smallIcon = array.getResourceId(R_Hide.styleable.AccountAuthenticator_smallIcon.get(), 0);
			int accountPreferences = array.getResourceId(R_Hide.styleable.AccountAuthenticator_accountPreferences.get(), 0);
			boolean customTokens = array.getBoolean(R_Hide.styleable.AccountAuthenticator_customTokens.get(), false);
			if (TextUtils.isEmpty(accountType)) {
				return null;
			}
			return new AuthenticatorDescription(accountType, packageName, label, icon, smallIcon, accountPreferences,
					customTokens);
		} finally {
			array.recycle();
		}
	}


	@Override
	public AuthenticatorDescription[] getAuthenticatorTypes(int userId) {
		synchronized (cache) {
			AuthenticatorDescription[] descArray = new AuthenticatorDescription[cache.authenticators.size()];
			int i = 0;
			for (AuthenticatorInfo info : cache.authenticators.values()) {
				descArray[i] = info.desc;
				i++;
			}
			return descArray;
		}
	}

	@Override
	public void getAccountsByFeatures(int userId, IAccountManagerResponse response, String type, String[] features) {
		if (response == null) throw new IllegalArgumentException("response is null");
		if (type == null) throw new IllegalArgumentException("accountType is null");
		AuthenticatorInfo info = getAuthenticatorInfo(type);
		if (info == null) {
			Bundle bundle = new Bundle();
			bundle.putParcelableArray(AccountManager.KEY_ACCOUNTS, new Account[0]);
			try {
				response.onResult(bundle);
			} catch (RemoteException e) {
				e.printStackTrace();
			}
			return;
		}

		if (features == null || features.length == 0) {
			Bundle bundle = new Bundle();
			bundle.putParcelableArray(AccountManager.KEY_ACCOUNTS, getAccounts(userId, type));
			try {
				response.onResult(bundle);
			} catch (RemoteException e) {
				e.printStackTrace();
			}
		} else {
			new GetAccountsByTypeAndFeatureSession(response, userId, info, features).bind();
		}
	}

	@Override
	public final String getPreviousName(int userId, Account account) {
		if (account == null) throw new IllegalArgumentException("account is null");
		synchronized (accountsByUserId) {
			String previousName = null;
			VAccount vAccount = getAccount(userId, account);
			if (vAccount != null) {
				previousName = vAccount.previousName;
			}
			return previousName;
		}
	}


	@Override
	public Account[] getAccounts(int userId, String type) {
		List<Account> accountList = getAccountList(userId, type);
		return accountList.toArray(new Account[accountList.size()]);
	}


	private List<Account> getAccountList(int userId, String type) {
		synchronized (accountsByUserId) {
			List<Account> accounts = new ArrayList<>();
			List<VAccount> vAccounts = accountsByUserId.get(userId);
			if (vAccounts != null) {
				for (VAccount vAccount : vAccounts) {
					if (type == null || vAccount.type.equals(type)) {
						accounts.add(new Account(vAccount.name, vAccount.type));
					}
				}
			}
			return accounts;
		}
	}

	@Override
	public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
		try {
			return super.onTransact(code, data, reply, flags);
		} catch (Throwable e) {
			e.printStackTrace();
			throw e;
		}
	}

	@Override
	public final void getAuthToken(final int userId, final IAccountManagerResponse response, final Account account, final String authTokenType, final boolean notifyOnAuthFailure, boolean expectActivityLaunch, final Bundle loginOptions) {
		if (response == null) {
			throw new IllegalArgumentException("response is null");
		}
		try {
			if (account == null) {
				VLog.w(TAG, "getAuthToken called with null account");
				response.onError(ERROR_CODE_BAD_ARGUMENTS, "account is null");
				return;
			}
			if (authTokenType == null) {
				VLog.w(TAG, "getAuthToken called with null authTokenType");
				response.onError(ERROR_CODE_BAD_ARGUMENTS, "authTokenType is null");
				return;
			}
		} catch (RemoteException e) {
			e.printStackTrace();
			return;
		}
		AuthenticatorInfo info = getAuthenticatorInfo(account.type);
		if (info == null) {
			try {
				response.onError(ERROR_CODE_BAD_ARGUMENTS, "account.type does not exist");
			} catch (RemoteException e) {
				e.printStackTrace();
			}
			return;
		}
		// Get the calling package. We will use it for the purpose of caching.
		final String callerPkg = loginOptions.getString(AccountManagerCompat.KEY_ANDROID_PACKAGE_NAME);
		final boolean customTokens = info.desc.customTokens;

		loginOptions.putInt(AccountManager.KEY_CALLER_UID, VBinder.getCallingUid());
		loginOptions.putInt(AccountManager.KEY_CALLER_PID, Binder.getCallingPid());
		if (notifyOnAuthFailure) {
			loginOptions.putBoolean(AccountManagerCompat.KEY_NOTIFY_ON_FAILURE, true);
		}
		if (!customTokens) {
			VAccount vAccount;
			synchronized (accountsByUserId) {
				vAccount = getAccount(userId, account);
			}
			String authToken = vAccount != null ? vAccount.authTokens.get(authTokenType) : null;
			if (authToken != null) {
				Bundle result = new Bundle();
				result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
				result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
				result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
				onResult(response, result);
				return;
			}
		}
		if (customTokens) {
			String authToken = getCustomAuthToken(userId, account, authTokenType, callerPkg);
			if (authToken != null) {
				Bundle result = new Bundle();
				result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
				result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
				result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
				onResult(response, result);
				return;
			}
		}
		new Session(response, userId, info, expectActivityLaunch, false, account.name) {

			@Override
			protected String toDebugString(long now) {
				return super.toDebugString(now) + ", getAuthToken"
						+ ", " + account
						+ ", authTokenType " + authTokenType
						+ ", loginOptions " + loginOptions
						+ ", notifyOnAuthFailure " + notifyOnAuthFailure;
			}

			@Override
			public void run() throws RemoteException {
				mAuthenticator.getAuthToken(this, account, authTokenType, loginOptions);
			}

			@Override
			public void onResult(Bundle result) throws RemoteException {
				if (result != null) {
					String authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
					if (authToken != null) {
						String name = result.getString(AccountManager.KEY_ACCOUNT_NAME);
						String type = result.getString(AccountManager.KEY_ACCOUNT_TYPE);
						if (TextUtils.isEmpty(type) || TextUtils.isEmpty(name)) {
							onError(AccountManager.ERROR_CODE_INVALID_RESPONSE,
									"the type and name should not be empty");
							return;
						}
						if (!customTokens) {
							synchronized (accountsByUserId) {
								VAccount account = getAccount(userId, name, type);
								if (account == null) {
									List<VAccount> accounts = accountsByUserId.get(userId);
									if (accounts == null) {
										accounts = new ArrayList<>();
										accountsByUserId.put(userId, accounts);
									}
									account = new VAccount(userId, new Account(name, type));
									accounts.add(account);
									saveAllAccounts();
								}
							}
						}
						long expiryMillis = result.getLong(
								AccountManagerCompat.KEY_CUSTOM_TOKEN_EXPIRY, 0L);
						if (customTokens
								&& expiryMillis > System.currentTimeMillis()) {
							AuthTokenRecord record = new AuthTokenRecord(userId, account, authTokenType, callerPkg, authToken, expiryMillis);
							synchronized (authTokenRecords) {
								authTokenRecords.remove(record);
								authTokenRecords.add(record);
							}
						}
					}
					Intent intent = result.getParcelable(AccountManager.KEY_INTENT);
					if (intent != null && notifyOnAuthFailure && !customTokens) {
						// TODO: send Signin error Notification
					}
				}
				super.onResult(result);
			}
		}.bind();
	}


	@Override
	public void setPassword(int userId, Account account, String password) {
		if (account == null) throw new IllegalArgumentException("account is null");
		setPasswordInternal(userId, account, password);
	}

	private void setPasswordInternal(int userId, Account account, String password) {
		synchronized (accountsByUserId) {
			VAccount vAccount = getAccount(userId, account);
			if (vAccount != null) {
				vAccount.password = password;
				vAccount.authTokens.clear();
				saveAllAccounts();
				synchronized (authTokenRecords) {
					Iterator<AuthTokenRecord> iterator = authTokenRecords.iterator();
					while (iterator.hasNext()) {
						AuthTokenRecord record = iterator.next();
						if (record.userId == userId && record.account.equals(account)) {
							iterator.remove();
						}
					}
				}
				sendAccountsChangedBroadcast(userId);
			}
		}
	}

	@Override
	public void setAuthToken(int userId, Account account, String authTokenType, String authToken) {
		if (account == null) throw new IllegalArgumentException("account is null");
		if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
		synchronized (accountsByUserId) {
			VAccount vAccount = getAccount(userId, account);
			if (vAccount != null) {
				// FIXME: cancelNotification
				vAccount.authTokens.put(authTokenType, authToken);
				this.saveAllAccounts();
			}
		}
	}


	@Override
	public void setUserData(int userId, Account account, String key, String value) {
		if (key == null) throw new IllegalArgumentException("key is null");
		if (account == null) throw new IllegalArgumentException("account is null");
		VAccount vAccount = getAccount(userId, account);
		if (vAccount != null) {
			synchronized (accountsByUserId) {
				vAccount.userDatas.put(key, value);
				saveAllAccounts();
			}
		}
	}


	@Override
	public void hasFeatures(int userId, IAccountManagerResponse response,
							final Account account, final String[] features) {
		if (response == null) throw new IllegalArgumentException("response is null");
		if (account == null) throw new IllegalArgumentException("account is null");
		if (features == null) throw new IllegalArgumentException("features is null");
		AuthenticatorInfo info = this.getAuthenticatorInfo(account.type);
		if (info == null) {
			try {
				response.onError(ERROR_CODE_BAD_ARGUMENTS, "account.type does not exist");
			} catch (RemoteException e) {
				e.printStackTrace();
			}
			return;
		}
		new Session(response, userId, info, false, true, account.name) {

			@Override
			public void run() throws RemoteException {
				try {
					mAuthenticator.hasFeatures(this, account, features);
				} catch (RemoteException e) {
					onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "remote exception");
				}
			}

			@Override
			public void onResult(Bundle result) throws RemoteException {
				IAccountManagerResponse response = getResponseAndClose();
				if (response != null) {
					try {
						if (result == null) {
							response.onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, "null bundle");
							return;
						}
						Log.v(TAG, getClass().getSimpleName() + " calling onResult() on response "
								+ response);
						final Bundle newResult = new Bundle();
						newResult.putBoolean(AccountManager.KEY_BOOLEAN_RESULT,
								result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT, false));
						response.onResult(newResult);
					} catch (RemoteException e) {
						// if the caller is dead then there is no one to care about remote exceptions
						Log.v(TAG, "failure while notifying response", e);
					}
				}
			}
		}.bind();
	}


	@Override
	public void updateCredentials(int userId, final IAccountManagerResponse response, final Account account,
								  final String authTokenType, final boolean expectActivityLaunch,
								  final Bundle loginOptions) {
		if (response == null) throw new IllegalArgumentException("response is null");
		if (account == null) throw new IllegalArgumentException("account is null");
		if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
		AuthenticatorInfo info = this.getAuthenticatorInfo(account.type);
		if (info == null) {
			try {
				response.onError(ERROR_CODE_BAD_ARGUMENTS, "account.type does not exist");
			} catch (RemoteException e) {
				e.printStackTrace();
			}
			return;
		}
		new Session(response, userId, info, expectActivityLaunch, false, account.name) {

			@Override
			public void run() throws RemoteException {
				mAuthenticator.updateCredentials(this, account, authTokenType, loginOptions);
			}

			@Override
			protected String toDebugString(long now) {
				if (loginOptions != null) loginOptions.keySet();
				return super.toDebugString(now) + ", updateCredentials"
						+ ", " + account
						+ ", authTokenType " + authTokenType
						+ ", loginOptions " + loginOptions;
			}

		}.bind();
	}

	@Override
	public String getPassword(int userId, Account account) {
		if (account == null) throw new IllegalArgumentException("account is null");
		synchronized (accountsByUserId) {
			VAccount vAccount = getAccount(userId, account);
			if (vAccount != null) {
				return vAccount.password;
			}
			return null;
		}
	}

	@Override
	public String getUserData(int userId, Account account, String key) {
		if (account == null) throw new IllegalArgumentException("account is null");
		if (key == null) throw new IllegalArgumentException("key is null");
		synchronized (accountsByUserId) {
			VAccount vAccount = getAccount(userId, account);
			if (vAccount != null) {
				return vAccount.userDatas.get(key);
			}
			return null;
		}
	}

	@Override
	public void editProperties(int userId, IAccountManagerResponse response, final String accountType,
							   final boolean expectActivityLaunch) {
		if (response == null) throw new IllegalArgumentException("response is null");
		if (accountType == null) throw new IllegalArgumentException("accountType is null");
		AuthenticatorInfo info = this.getAuthenticatorInfo(accountType);
		if (info == null) {
			try {
				response.onError(ERROR_CODE_BAD_ARGUMENTS, "account.type does not exist");
			} catch (RemoteException e) {
				e.printStackTrace();
			}
			return;
		}
		new Session(response, userId, info, expectActivityLaunch, true, null) {

			@Override
			public void run() throws RemoteException {
				mAuthenticator.editProperties(this, mAuthenticatorInfo.desc.type);
			}

			@Override
			protected String toDebugString(long now) {
				return super.toDebugString(now) + ", editProperties"
						+ ", accountType " + accountType;
			}

		}.bind();

	}


	@Override
	public void getAuthTokenLabel(int userId, IAccountManagerResponse response, final String accountType,
								  final String authTokenType) {
		if (accountType == null) throw new IllegalArgumentException("accountType is null");
		if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
		AuthenticatorInfo info = getAuthenticatorInfo(accountType);
		if (info == null) {
			try {
				response.onError(ERROR_CODE_BAD_ARGUMENTS, "account.type does not exist");
			} catch (RemoteException e) {
				e.printStackTrace();
			}
			return;
		}
		new Session(response, userId, info, false, false, null) {

			@Override
			public void run() throws RemoteException {
				mAuthenticator.getAuthTokenLabel(this, authTokenType);
			}

			@Override
			public void onResult(Bundle result) throws RemoteException {
				if (result != null) {
					String label = result.getString(AccountManager.KEY_AUTH_TOKEN_LABEL);
					Bundle bundle = new Bundle();
					bundle.putString(AccountManager.KEY_AUTH_TOKEN_LABEL, label);
					super.onResult(bundle);
				} else {
					super.onResult(null);
				}
			}
		}.bind();
	}

	public void confirmCredentials(int userId, IAccountManagerResponse response, final Account account, final Bundle options, final boolean expectActivityLaunch) {
		if (response == null) throw new IllegalArgumentException("response is null");
		if (account == null) throw new IllegalArgumentException("account is null");
		AuthenticatorInfo info = getAuthenticatorInfo(account.type);
		if (info == null) {
			try {
				response.onError(ERROR_CODE_BAD_ARGUMENTS, "account.type does not exist");
			} catch (RemoteException e) {
				e.printStackTrace();
			}
			return;
		}
		new Session(response, userId, info, expectActivityLaunch, true, account.name, true, true) {

			@Override
			public void run() throws RemoteException {
				mAuthenticator.confirmCredentials(this, account, options);
			}

		}.bind();

	}

	@Override
	public void addAccount(int userId, final IAccountManagerResponse response, final String accountType,
						   final String authTokenType, final String[] requiredFeatures,
						   final boolean expectActivityLaunch, final Bundle optionsIn) {
		if (response == null) throw new IllegalArgumentException("response is null");
		if (accountType == null) throw new IllegalArgumentException("accountType is null");
		AuthenticatorInfo info = getAuthenticatorInfo(accountType);
		if (info == null) {
			try {
				response.onError(ERROR_CODE_BAD_ARGUMENTS, "account.type does not exist");
			} catch (RemoteException e) {
				e.printStackTrace();
			}
			return;
		}
		new Session(response, userId, info, expectActivityLaunch, true, null, false, true) {

			@Override
			public void run() throws RemoteException {
				mAuthenticator.addAccount(this, mAuthenticatorInfo.desc.type, authTokenType, requiredFeatures,
						optionsIn);
			}

			@Override
			protected String toDebugString(long now) {
				return super.toDebugString(now) + ", addAccount"
						+ ", accountType " + accountType
						+ ", requiredFeatures "
						+ (requiredFeatures != null
						? TextUtils.join(",", requiredFeatures)
						: null);
			}

		}.bind();

	}

	@Override
	public boolean addAccountExplicitly(int userId, Account account, String password, Bundle extras) {
		if (account == null) throw new IllegalArgumentException("account is null");
		return insertAccountIntoDatabase(userId, account, password, extras);
	}

	@Override
	public boolean removeAccountExplicitly(int userId, Account account) {
		return account != null && removeAccountInternal(userId, account);
	}

	@Override
	public void renameAccount(int userId, IAccountManagerResponse response, Account accountToRename, String newName) {
		if (accountToRename == null) throw new IllegalArgumentException("account is null");
		Account resultingAccount = renameAccountInternal(userId, accountToRename, newName);
		Bundle result = new Bundle();
		result.putString(AccountManager.KEY_ACCOUNT_NAME, resultingAccount.name);
		result.putString(AccountManager.KEY_ACCOUNT_TYPE, resultingAccount.type);
		try {
			response.onResult(result);
		} catch (RemoteException e) {
			Log.w(TAG, e.getMessage());
		}
	}

	@Override
	public void removeAccount(final int userId, IAccountManagerResponse response, final Account account,
							  boolean expectActivityLaunch) {
		if (response == null) throw new IllegalArgumentException("response is null");
		if (account == null) throw new IllegalArgumentException("account is null");
		AuthenticatorInfo info = this.getAuthenticatorInfo(account.type);
		if (info == null) {
			try {
				response.onError(ERROR_CODE_BAD_ARGUMENTS, "account.type does not exist");
			} catch (RemoteException e) {
				e.printStackTrace();
			}
			return;
		}
		// FIXME: Cancel Notification

		new Session(response, userId, info, expectActivityLaunch, true, account.name) {
			@Override
			protected String toDebugString(long now) {
				return super.toDebugString(now) + ", removeAccount"
						+ ", account " + account;
			}

			@Override
			public void run() throws RemoteException {
				mAuthenticator.getAccountRemovalAllowed(this, account);
			}

			@Override
			public void onResult(Bundle result) throws RemoteException {
				if (result != null && result.containsKey(AccountManager.KEY_BOOLEAN_RESULT)
						&& !result.containsKey(AccountManager.KEY_INTENT)) {
					final boolean removalAllowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
					if (removalAllowed) {
						removeAccountInternal(userId, account);
					}
					IAccountManagerResponse response = getResponseAndClose();
					if (response != null) {
						Log.v(TAG, getClass().getSimpleName() + " calling onResult() on response "
								+ response);
						Bundle result2 = new Bundle();
						result2.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, removalAllowed);
						try {
							response.onResult(result2);
						} catch (RemoteException e) {
							e.printStackTrace();
						}
					}
				}
				super.onResult(result);
			}

		}.bind();

	}

	@Override
	public void clearPassword(int userId, Account account) {
		if (account == null) throw new IllegalArgumentException("account is null");
		setPasswordInternal(userId, account, null);
	}

	private boolean removeAccountInternal(int userId, Account account) {
		List<VAccount> accounts = accountsByUserId.get(userId);
		if (accounts != null) {
			Iterator<VAccount> iterator = accounts.iterator();
			while (iterator.hasNext()) {
				VAccount vAccount = iterator.next();
				if (userId == vAccount.userId
						&& TextUtils.equals(vAccount.name, account.name)
						&& TextUtils.equals(account.type, vAccount.type)) {
					iterator.remove();
					saveAllAccounts();
					sendAccountsChangedBroadcast(userId);
					return true;
				}
			}
		}
		return false;
	}


	@Override
	public boolean accountAuthenticated(int userId, final Account account) {
		if (account == null) {
			throw new IllegalArgumentException("account is null");
		}
		synchronized (accountsByUserId) {
			VAccount vAccount = getAccount(userId, account);
			if (vAccount != null) {
				vAccount.lastAuthenticatedTime = System.currentTimeMillis();
				saveAllAccounts();
				return true;
			}
			return false;
		}
	}

	@Override
	public void invalidateAuthToken(int userId, String accountType, String authToken) {
		if (accountType == null) throw new IllegalArgumentException("accountType is null");
		if (authToken == null) throw new IllegalArgumentException("authToken is null");
		synchronized (accountsByUserId) {
			List<VAccount> accounts = accountsByUserId.get(userId);
			if (accounts != null) {
				boolean changed = false;
				for (VAccount account : accounts) {
					if (account.type.equals(accountType)) {
						account.authTokens.values().remove(authToken);
						changed = true;
					}
				}
				if (changed) {
					saveAllAccounts();
				}
			}
			synchronized (authTokenRecords) {
				Iterator<AuthTokenRecord> iterator = authTokenRecords.iterator();
				while (iterator.hasNext()) {
					AuthTokenRecord record = iterator.next();
					if (record.userId == userId && record.authTokenType.equals(accountType)
							&& record.authToken.equals(authToken)) {
						iterator.remove();
					}
				}
			}
		}
	}


	private Account renameAccountInternal(int userId, Account accountToRename, String newName) {
		// TODO: Cancel Notification
		synchronized (accountsByUserId) {
			VAccount vAccount = getAccount(userId, accountToRename);
			if (vAccount != null) {
				vAccount.previousName = vAccount.name;
				vAccount.name = newName;
				saveAllAccounts();
				Account newAccount = new Account(vAccount.name, vAccount.type);
				synchronized (authTokenRecords) {
					for (AuthTokenRecord record : authTokenRecords) {
						if (record.userId == userId && record.account.equals(accountToRename)) {
							record.account = newAccount;
						}
					}
				}
				sendAccountsChangedBroadcast(userId);
				return newAccount;
			}
		}
		return accountToRename;
	}

	@Override
	public String peekAuthToken(int userId, Account account, String authTokenType) {
		if (account == null) throw new IllegalArgumentException("account is null");
		if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
		synchronized (accountsByUserId) {
			VAccount vAccount = getAccount(userId, account);
			if (vAccount != null) {
				return vAccount.authTokens.get(authTokenType);
			}
			return null;
		}
	}


	private String getCustomAuthToken(int userId, Account account, String authTokenType, String packageName) {
		AuthTokenRecord record = new AuthTokenRecord(userId, account, authTokenType, packageName);
		String authToken = null;
		long now = System.currentTimeMillis();
		synchronized (authTokenRecords) {
			Iterator<AuthTokenRecord> iterator = authTokenRecords.iterator();
			while (iterator.hasNext()) {
				AuthTokenRecord one = iterator.next();
				if (one.expiryEpochMillis > 0 && one.expiryEpochMillis < now) {
					iterator.remove();
				} else if (record.equals(one)) {
					authToken = record.authToken;
				}
			}
		}
		return authToken;
	}

	private void onResult(IAccountManagerResponse response, Bundle result) {
		try {
			response.onResult(result);
		} catch (RemoteException e) {
			// if the caller is dead then there is no one to care about remote
			// exceptions
			e.printStackTrace();
		}
	}

	private AuthenticatorInfo getAuthenticatorInfo(String type) {
		synchronized (cache) {
			return type == null ? null : cache.authenticators.get(type);
		}
	}


	private VAccount getAccount(int userId, Account account) {
		return this.getAccount(userId, account.name, account.type);
	}

	private boolean insertAccountIntoDatabase(int userId, Account account, String password, Bundle extras) {
		if (account == null) {
			return false;
		}
		synchronized (accountsByUserId) {
			VAccount vAccount = new VAccount(userId, account);
			vAccount.password = password;
			// convert the [Bundle] to [Map<String, String>]
			if (extras != null) {
				for (String key : extras.keySet()) {
					Object value = extras.get(key);
					if (value instanceof String) {
						vAccount.userDatas.put(key, (String) value);
					}
				}
			}
			List<VAccount> accounts = accountsByUserId.get(userId);
			if (accounts == null) {
				accounts = new ArrayList<>();
				accountsByUserId.put(userId, accounts);
			}
			accounts.add(vAccount);
			saveAllAccounts();
			sendAccountsChangedBroadcast(vAccount.userId);
			return true;
		}
	}

	private void sendAccountsChangedBroadcast(int userId) {
		Intent intent = new Intent(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION);
		VActivityManagerService.get().sendBroadcastAsUser(intent, new VUserHandle(userId));
		broadcastCheckInNowIfNeed(userId);
	}

	private void broadcastCheckInNowIfNeed(int userId) {
		long time = System.currentTimeMillis();
		if (Math.abs(time - lastAccountChangeTime) > CHECK_IN_TIME) {
			lastAccountChangeTime = time;
			saveAllAccounts();
			Intent intent = new Intent("android.server.checkin.CHECKIN_NOW");
			VActivityManagerService.get().sendBroadcastAsUser(intent, new VUserHandle(userId));
		}
	}

	/**
	 * Serializing all accounts
	 */
	private void saveAllAccounts() {
		File accountFile = VEnvironment.getAccountConfigFile();
		Parcel dest = Parcel.obtain();
		try {
			dest.writeInt(1);
			List<VAccount> accounts = new ArrayList<>();
			for (int i = 0; i < this.accountsByUserId.size(); i++) {
				List<VAccount> list = this.accountsByUserId.valueAt(i);
				if (list != null) {
					accounts.addAll(list);
				}
			}
			dest.writeInt(accounts.size());
			for (VAccount account : accounts) {
				account.writeToParcel(dest, 0);
			}
			dest.writeLong(lastAccountChangeTime);
			FileOutputStream fileOutputStream = new FileOutputStream(accountFile);
			fileOutputStream.write(dest.marshall());
			fileOutputStream.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		dest.recycle();
	}

	/**
	 * Read all accounts from file.
	 */
	private void readAllAccounts() {
		File accountFile = VEnvironment.getAccountConfigFile();
		refreshAuthenticatorCache(null);
		if (accountFile.exists()) {
			accountsByUserId.clear();
			Parcel dest = Parcel.obtain();
			try {
				FileInputStream is = new FileInputStream(accountFile);
				byte[] bytes = new byte[(int) accountFile.length()];
				int readLength = is.read(bytes);
				is.close();
				if (readLength != bytes.length) {
					throw new IOException(String.format(Locale.ENGLISH, "Expect length %d, but got %d.", bytes.length, readLength));
				}
				dest.unmarshall(bytes, 0, bytes.length);
				dest.setDataPosition(0);
				dest.readInt(); // skip the magic
				int size = dest.readInt(); // the VAccount's size we need to read
				boolean invalid = false;
				while (size-- > 0) {
					VAccount account = new VAccount(dest);
					VLog.d(TAG, "Reading account : " + account.type);
					AuthenticatorInfo info = cache.authenticators.get(account.type);
					if (info != null) {
						List<VAccount> accounts = accountsByUserId.get(account.userId);
						if (accounts == null) {
							accounts = new ArrayList<>();
							accountsByUserId.put(account.userId, accounts);
						}
						accounts.add(account);
					} else {
						invalid = true;
					}
				}
				lastAccountChangeTime = dest.readLong();
				if (invalid) {
					saveAllAccounts();
				}
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				dest.recycle();
			}
		}
	}


	private VAccount getAccount(int userId, String accountName, String accountType) {
		List<VAccount> accounts = accountsByUserId.get(userId);
		if (accounts != null) {
			for (VAccount account : accounts) {
				if (TextUtils.equals(account.name, accountName) && TextUtils.equals(account.type, accountType)) {
					return account;
				}
			}
		}
		return null;
	}


	public void refreshAuthenticatorCache(String packageName) {
		cache.authenticators.clear();
		Intent intent = new Intent(AccountManager.ACTION_AUTHENTICATOR_INTENT);
		if (packageName != null) {
			intent.setPackage(packageName);
		}
		generateServicesMap(
				VPackageManagerService.get().queryIntentServices(intent, null, PackageManager.GET_META_DATA, 0),
				cache.authenticators, new AppAccountParser());
	}

	private void generateServicesMap(List<ResolveInfo> services, Map<String, AuthenticatorInfo> map,
									 IAccountParser accountParser) {
		for (ResolveInfo info : services) {
			XmlResourceParser parser = accountParser.getParser(mContext, info.serviceInfo,
					AccountManager.AUTHENTICATOR_META_DATA_NAME);
			if (parser != null) {
				try {
					AttributeSet attributeSet = Xml.asAttributeSet(parser);
					int type;
					while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
						// Nothing to do
					}
					if (AccountManager.AUTHENTICATOR_ATTRIBUTES_NAME.equals(parser.getName())) {
						AuthenticatorDescription desc = parseAuthenticatorDescription(
								accountParser.getResources(mContext, info.serviceInfo.applicationInfo),
								info.serviceInfo.packageName, attributeSet);
						if (desc != null) {
							map.put(desc.type, new AuthenticatorInfo(desc, info.serviceInfo));
						}
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
	}

	final static class AuthTokenRecord {
		public int userId;
		public Account account;
		public long expiryEpochMillis;
		public String authToken;
		private String authTokenType;
		private String packageName;

		AuthTokenRecord(int userId, Account account, String authTokenType, String packageName, String authToken,
						long expiryEpochMillis) {
			this.userId = userId;
			this.account = account;
			this.authTokenType = authTokenType;
			this.packageName = packageName;
			this.authToken = authToken;
			this.expiryEpochMillis = expiryEpochMillis;
		}

		AuthTokenRecord(int userId, Account account, String authTokenType, String packageName) {
			this.userId = userId;
			this.account = account;
			this.authTokenType = authTokenType;
			this.packageName = packageName;
		}

		@Override
		public boolean equals(Object o) {
			if (this == o)
				return true;
			if (o == null || getClass() != o.getClass())
				return false;
			AuthTokenRecord that = (AuthTokenRecord) o;
			return userId == that.userId
					&& account.equals(that.account)
					&& authTokenType.equals(that.authTokenType)
					&& packageName.equals(that.packageName);
		}

		@Override
		public int hashCode() {
			return ((this.userId * 31 + this.account.hashCode()) * 31
					+ this.authTokenType.hashCode()) * 31
					+ this.packageName.hashCode();
		}
	}

	private final class AuthenticatorInfo {
		final AuthenticatorDescription desc;
		final ServiceInfo serviceInfo;

		AuthenticatorInfo(AuthenticatorDescription desc, ServiceInfo info) {
			this.desc = desc;
			this.serviceInfo = info;
		}
	}

	private final class AuthenticatorCache {
		final Map<String, AuthenticatorInfo> authenticators = new HashMap<>();
	}

	private abstract class Session extends IAccountAuthenticatorResponse.Stub
			implements IBinder.DeathRecipient, ServiceConnection {
		final int mUserId;
		final AuthenticatorInfo mAuthenticatorInfo;
		private final boolean mStripAuthTokenFromResult;
		public int mNumResults;
		IAccountAuthenticator mAuthenticator;
		private IAccountManagerResponse mResponse;
		private boolean mExpectActivityLaunch;
		private long mCreationTime;
		private String mAccountName;
		private boolean mAuthDetailsRequired;
		private boolean mUpdateLastAuthenticatedTime;
		private int mNumRequestContinued;
		private int mNumErrors;


		Session(IAccountManagerResponse response, int userId, AuthenticatorInfo info, boolean expectActivityLaunch, boolean stripAuthTokenFromResult, String accountName, boolean authDetailsRequired, boolean updateLastAuthenticatedTime) {
			if (info == null) throw new IllegalArgumentException("accountType is null");
			this.mStripAuthTokenFromResult = stripAuthTokenFromResult;
			this.mResponse = response;
			this.mUserId = userId;
			this.mAuthenticatorInfo = info;
			this.mExpectActivityLaunch = expectActivityLaunch;
			this.mCreationTime = SystemClock.elapsedRealtime();
			this.mAccountName = accountName;
			this.mAuthDetailsRequired = authDetailsRequired;
			this.mUpdateLastAuthenticatedTime = updateLastAuthenticatedTime;
			synchronized (mSessions) {
				mSessions.put(toString(), this);
			}
			if (response != null) {
				try {
					response.asBinder().linkToDeath(this, 0 /* flags */);
				} catch (RemoteException e) {
					mResponse = null;
					binderDied();
				}
			}
		}

		Session(IAccountManagerResponse response, int userId, AuthenticatorInfo info, boolean expectActivityLaunch, boolean stripAuthTokenFromResult, String accountName) {
			this(response, userId, info, expectActivityLaunch, stripAuthTokenFromResult, accountName, false, false);
		}

		IAccountManagerResponse getResponseAndClose() {
			if (mResponse == null) {
				// this session has already been closed
				return null;
			}
			IAccountManagerResponse response = mResponse;
			close(); // this clears mResponse so we need to save the response before this call
			return response;
		}

		private void close() {
			synchronized (mSessions) {
				if (mSessions.remove(toString()) == null) {
					// the session was already closed, so bail out now
					return;
				}
			}
			if (mResponse != null) {
				// stop listening for response deaths
				mResponse.asBinder().unlinkToDeath(this, 0 /* flags */);

				// clear this so that we don't accidentally send any further results
				mResponse = null;
			}
			unbind();
		}

		@Override
		public void onServiceConnected(ComponentName name, IBinder service) {
			mAuthenticator = IAccountAuthenticator.Stub.asInterface(service);
			try {
				run();
			} catch (RemoteException e) {
				onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION,
						"remote exception");
			}
		}

		@Override
		public void onRequestContinued() {
			mNumRequestContinued++;
		}

		@Override
		public void onError(int errorCode, String errorMessage) {
			mNumErrors++;
			IAccountManagerResponse response = getResponseAndClose();
			if (response != null) {
				Log.v(TAG, getClass().getSimpleName()
						+ " calling onError() on response " + response);
				try {
					response.onError(errorCode, errorMessage);
				} catch (RemoteException e) {
					Log.v(TAG, "Session.onError: caught RemoteException while responding", e);
				}
			} else {
				Log.v(TAG, "Session.onError: already closed");
			}
		}

		@Override
		public void onServiceDisconnected(ComponentName name) {
			mAuthenticator = null;
			IAccountManagerResponse response = getResponseAndClose();
			if (response != null) {
				try {
					response.onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION,
							"disconnected");
				} catch (RemoteException e) {
					Log.v(TAG, "Session.onServiceDisconnected: "
							+ "caught RemoteException while responding", e);
				}
			}
		}

		@Override
		public void onResult(Bundle result) throws RemoteException {
			mNumResults++;
			if (result != null) {
				boolean isSuccessfulConfirmCreds = result.getBoolean(
						AccountManager.KEY_BOOLEAN_RESULT, false);
				boolean isSuccessfulUpdateCredsOrAddAccount =
						result.containsKey(AccountManager.KEY_ACCOUNT_NAME)
								&& result.containsKey(AccountManager.KEY_ACCOUNT_TYPE);
				// We should only update lastAuthenticated time, if
				// mUpdateLastAuthenticatedTime is true and the confirmRequest
				// or updateRequest was successful
				boolean needUpdate = mUpdateLastAuthenticatedTime
						&& (isSuccessfulConfirmCreds || isSuccessfulUpdateCredsOrAddAccount);
				if (needUpdate || mAuthDetailsRequired) {
					synchronized (accountsByUserId) {
						VAccount account = getAccount(mUserId, mAccountName, mAuthenticatorInfo.desc.type);
						if (needUpdate && account != null) {
							account.lastAuthenticatedTime = System.currentTimeMillis();
							saveAllAccounts();
						}
						if (mAuthDetailsRequired) {
							long lastAuthenticatedTime = -1;
							if (account != null) {
								lastAuthenticatedTime = account.lastAuthenticatedTime;
							}
							result.putLong(AccountManagerCompat.KEY_LAST_AUTHENTICATED_TIME, lastAuthenticatedTime);
						}
					}
				}
			}
			if (result != null
					&& !TextUtils.isEmpty(result.getString(AccountManager.KEY_AUTHTOKEN))) {
//				String accountName = result.getString(AccountManager.KEY_ACCOUNT_NAME);
//				String accountType = result.getString(AccountManager.KEY_ACCOUNT_TYPE);
//				if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
//					Account account = new Account(accountName, accountType);
//					FIXME: Cancel Notification
//				}
			}
			Intent intent = null;
			if (result != null) {
				intent = result.getParcelable(AccountManager.KEY_INTENT);
			}
			IAccountManagerResponse response;
			if (mExpectActivityLaunch && result != null
					&& result.containsKey(AccountManager.KEY_INTENT)) {
				response = mResponse;
			} else {
				response = getResponseAndClose();
			}
			if (response != null) {
				try {
					if (result == null) {
						Log.v(TAG, getClass().getSimpleName()
								+ " calling onError() on response " + response);
						response.onError(AccountManager.ERROR_CODE_INVALID_RESPONSE,
								"null bundle returned");
					} else {
						if (mStripAuthTokenFromResult) {
							result.remove(AccountManager.KEY_AUTHTOKEN);
						}
						Log.v(TAG, getClass().getSimpleName()
								+ " calling onResult() on response " + response);
						if ((result.getInt(AccountManager.KEY_ERROR_CODE, -1) > 0) &&
								(intent == null)) {
							// All AccountManager error codes are greater than 0
							response.onError(result.getInt(AccountManager.KEY_ERROR_CODE),
									result.getString(AccountManager.KEY_ERROR_MESSAGE));
						} else {
							response.onResult(result);
						}
					}
				} catch (RemoteException e) {
					// if the caller is dead then there is no one to care about remote exceptions
					Log.v(TAG, "failure while notifying response", e);
				}
			}
		}

		public abstract void run() throws RemoteException;

		void bind() {
			Log.v(TAG, "initiating bind to authenticator type " + mAuthenticatorInfo.desc.type);
			Intent intent = new Intent();
			intent.setAction(AccountManager.ACTION_AUTHENTICATOR_INTENT);
			intent.setClassName(mAuthenticatorInfo.serviceInfo.packageName, mAuthenticatorInfo.serviceInfo.name);
			intent.putExtra("_VA_|_user_id_", mUserId);

			if (!mContext.bindService(intent, this, Context.BIND_AUTO_CREATE)) {
				Log.d(TAG, "bind attempt failed for " + toDebugString());
				onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "bind failure");
			}
		}

		protected String toDebugString() {
			return toDebugString(SystemClock.elapsedRealtime());
		}

		protected String toDebugString(long now) {
			return "Session: expectLaunch " + mExpectActivityLaunch
					+ ", connected " + (mAuthenticator != null)
					+ ", stats (" + mNumResults + "/" + mNumRequestContinued
					+ "/" + mNumErrors + ")"
					+ ", lifetime " + ((now - mCreationTime) / 1000.0);
		}

		private void unbind() {
			if (mAuthenticator != null) {
				mAuthenticator = null;
				mContext.unbindService(this);
			}
		}

		@Override
		public void binderDied() {
			mResponse = null;
			close();
		}
	}

	private class GetAccountsByTypeAndFeatureSession extends Session {
		private final String[] mFeatures;
		private volatile Account[] mAccountsOfType = null;
		private volatile ArrayList<Account> mAccountsWithFeatures = null;
		private volatile int mCurrentAccount = 0;

		public GetAccountsByTypeAndFeatureSession(IAccountManagerResponse response, int userId, AuthenticatorInfo info, String[] features) {
			super(response, userId, info, false /* expectActivityLaunch */,
					true /* stripAuthTokenFromResult */, null /* accountName */);
			mFeatures = features;
		}

		@Override
		public void run() throws RemoteException {
			mAccountsOfType = getAccounts(mUserId, mAuthenticatorInfo.desc.type);
			// check whether each account matches the requested features
			mAccountsWithFeatures = new ArrayList<Account>(mAccountsOfType.length);
			mCurrentAccount = 0;

			checkAccount();
		}

		public void checkAccount() {
			if (mCurrentAccount >= mAccountsOfType.length) {
				sendResult();
				return;
			}

			final IAccountAuthenticator accountAuthenticator = mAuthenticator;
			if (accountAuthenticator == null) {
				// It is possible that the authenticator has died, which is indicated by
				// mAuthenticator being set to null. If this happens then just abort.
				// There is no need to send back a result or error in this case since
				// that already happened when mAuthenticator was cleared.
				Log.v(TAG, "checkAccount: aborting session since we are no longer"
						+ " connected to the authenticator, " + toDebugString());
				return;
			}
			try {
				accountAuthenticator.hasFeatures(this, mAccountsOfType[mCurrentAccount], mFeatures);
			} catch (RemoteException e) {
				onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "remote exception");
			}
		}

		@Override
		public void onResult(Bundle result) {
			mNumResults++;
			if (result == null) {
				onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, "null bundle");
				return;
			}
			if (result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT, false)) {
				mAccountsWithFeatures.add(mAccountsOfType[mCurrentAccount]);
			}
			mCurrentAccount++;
			checkAccount();
		}

		public void sendResult() {
			IAccountManagerResponse response = getResponseAndClose();
			if (response != null) {
				try {
					Account[] accounts = new Account[mAccountsWithFeatures.size()];
					for (int i = 0; i < accounts.length; i++) {
						accounts[i] = mAccountsWithFeatures.get(i);
					}
					if (Log.isLoggable(TAG, Log.VERBOSE)) {
						Log.v(TAG, getClass().getSimpleName() + " calling onResult() on response "
								+ response);
					}
					Bundle result = new Bundle();
					result.putParcelableArray(AccountManager.KEY_ACCOUNTS, accounts);
					response.onResult(result);
				} catch (RemoteException e) {
					// if the caller is dead then there is no one to care about remote exceptions
					Log.v(TAG, "failure while notifying response", e);
				}
			}
		}


		@Override
		protected String toDebugString(long now) {
			return super.toDebugString(now) + ", getAccountsByTypeAndFeatures"
					+ ", " + (mFeatures != null ? TextUtils.join(",", mFeatures) : null);
		}
	}

}
